package za.org.jcicada.genss.impl;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.log4j.Logger;

import za.org.jcicada.comms.IAcceptListener;
import za.org.jcicada.comms.IAcceptor;
import za.org.jcicada.comms.impl.AcceptorFactory;
import za.org.jcicada.comms.impl.tcp.TcpSelector;
import za.org.jcicada.genss.IGenSS;
import za.org.jcicada.genss.ITerminateServiceListener;
import za.org.jcicada.genss.ServiceURN;
import za.org.jcicada.genss.SystemURN;
import za.org.jcicada.genss.alarm.IAlarmSubject;
import za.org.jcicada.genss.alarm.impl.AlarmAcceptListener;
import za.org.jcicada.genss.alarm.impl.AlarmSubject;
import za.org.jcicada.genss.lifecycle.ILifeCycleServer;
import za.org.jcicada.genss.lifecycle.LogLevel;
import za.org.jcicada.genss.lifecycle.impl.LifeCycleAcceptListener;
import za.org.jcicada.genss.lifecycle.impl.LifeCycleServer;
import za.org.jcicada.genss.messages.Result;
import za.org.jcicada.genss.messages.SuccessCode;
import za.org.jcicada.genss.sensor.ISensor;
import za.org.jcicada.genss.sensor.ISensorSubject;
import za.org.jcicada.genss.sensor.ISensorValue;
import za.org.jcicada.genss.sensor.SensorURN;
import za.org.jcicada.genss.sensor.impl.InvalidSensorValue;
import za.org.jcicada.genss.sensor.impl.Sensor;
import za.org.jcicada.genss.sensor.impl.SensorAcceptListener;
import za.org.jcicada.genss.sensor.impl.SensorSubject;
import za.org.jcicada.genss.sensor.impl.SensorValue;
import za.org.jcicada.genss.sensor.impl.StatusStrategyFactory;
import za.org.jcicada.genss.sensor.impl.TypeDiscrete;

/**
 * GenSS is the Generic Software Subsystem. It aggregates a sensor subject, an
 * alarm subject and a life cycle server. It uses two concurrent state machines
 * (state and mode) to control itself. The state machines are implemented using
 * the State Pattern and state changes are triggered by inserting state objects
 * into a blocking queue. Therefore each state machine runs in its own thread
 * and one state is guaranteed to exit before the next state is started.
 * 
 * @author rcrida
 */
public abstract class GenSS implements IGenSS {

//     static {
//         Logger.getRootLogger().setLevel(Level.TRACE);
//         ConsoleAppender appender = new ConsoleAppender(new PatternLayout("%l - %m\n"));
//         BasicConfigurator.configure(appender);
//     }

    /** Logger object used for logging messages. */
    private static final Log logger = LogFactory.getLog(GenSS.class);

    /**
     * Thread class for GenSS Mode state machine
     */
    public class ModeRunner extends BaseRunner {

        /** Handle to the context of the state machine */
        protected GenSS context;

        public ModeRunner(GenSS context) {
        	super("Thread-" + ModeRunner.class.getCanonicalName());
            this.context = context;
        }

        protected void strategy() throws InterruptedException {
            ModeEnum newMode = modeQueue.take();
            logger.trace(String.format("Entering mode %s", newMode));
            ISensorValue value = new SensorValue(newMode.toString());
            logger.info(String.format("Changing mode to %s", newMode.toString()));
            try {
                if (mode != null) {
                    mode.onExit(context);
                }
                modeSensor.setValue(value);
                switch(newMode) {
                case INITIALIZE: mode = initialize; break;
                case MANAGE: mode = manage; break;
                case SHUTDOWN: mode = shutdown; break;
                case SHUTDOWN_COMPLETE: mode = shutdownComplete; break;
                }
                mode.onEnter(context);
            } catch (Exception ex) {
                logger.error("Exception in ModeRunner", ex);
            }
        }
    }

    /** The sensor subject. */
    protected SensorSubject sensorSubject;

    /** The alarm subject. */
    protected AlarmSubject alarmSubject;

    /** The lifecycle server */
    protected LifeCycleServer lifeCycleServer;

    /** Restart flag used to indicate if a restart is required after returning
     * from waitHalt(). */
    private boolean restart;
    
    /** Semaphore used to notify when shutdown has started. */
    private final Semaphore shutdownStartedSemaphore = new Semaphore(0);

    /** Semaphore used to notify when shutdown has completed. Can't use
     * wait/notify because the other thread may not yet be waiting when notify
     * is called and hence it can fail. */
    private final Semaphore shutdownCompleteSemaphore = new Semaphore(0);
    
    /** Semaphore by shutdown hook for notifying it that shutdown has completed. */
    private final Semaphore shutdownInterruptedSemaphore = new Semaphore(0);

    /** List of acceptors that are waiting to accept connections. */
    private Map<IAcceptor, ServiceURN> acceptorMap = new HashMap<IAcceptor, ServiceURN>();

    /** Thread for mode state machine. */
    private ModeRunner modeThread;

    /** Blocking queue for Mode state machine. */
    private BlockingQueue<ModeEnum> modeQueue = new LinkedBlockingQueue<ModeEnum>();

    /** Maximum duration to wait for shutdown to complete gracefully */
    private final long SHUTDOWN_TIMEOUT = 5000;
    
    /** Duration to wait for tx queues to clear */
    private final long TX_QUEUE_CLEAR_DELAY = 100;

    /**
     * Enumeration for legal states for state sensor
     */
    public static enum State {
        INACTIVE, 
        OPERATIONAL;
    };

    /**
     * Enumeration for legal modes for mode sensor
     */
    public static enum ModeEnum {
        INITIALIZE,
        MANAGE,
        SHUTDOWN,
        SHUTDOWN_COMPLETE;
    };
    
    public class TerminateListener implements ITerminateServiceListener {

        public void terminateService(ServiceURN serviceURN) {
            deregisterService(serviceURN, null);
        }   
    }
    
    /** Object used to listen for service terminate events and propagates them */
    private TerminateListener terminateListener = new TerminateListener();

    /** Sensor representing the current state of the subsystem. */
    private Sensor stateSensor = new Sensor(TypeDiscrete.getInstance(State.class), "state", "GenSS State sensor", "");

    /** Sensor representing the current mode of the subsystem. */
    protected Sensor modeSensor = new Sensor(TypeDiscrete.getInstance(ModeEnum.class), "mode", "GenSS Mode sensor", "");

    /** URN representing the subsystem id of form system/type[.instance] */
    private SystemURN systemURN;
    
    /** URN representing the SensorSubject interface */
    private final ServiceURN sensorsURN;
    
    /** URN representing the AlarmSubject interface */
    private final ServiceURN alarmsURN;
    
    /** URN representing the LifeCycleServer interface */
    private final ServiceURN lifecycleURN;
    
    /** Timer object used by derived classes */
    private Timer timer;
    
    /**
     * Interface definition for mode states as per State Pattern.
     */
    interface Mode {

        /**
         * Action to take on entering state
         * 
         * @param context the context of the state machine
         */
        void onEnter(GenSS context) throws Exception;

        /**
         * Action to take on existing the state
         * 
         * @param context the context of the state machine
         */
        void onExit(GenSS context) throws Exception;
        
        /**
         * Try to shut down the state machine
         * @param context the context of the state machine
         * @param behaviour halt or restart
         * @return result object indicating success of request
         */
        Result shutdown(GenSS context, ILifeCycleServer.Shutdown behaviour);
        
        /**
         * Set the log level for the specified system
         * @param context the context of the state machine
         * @param level the desired log level
         * @return object describing the result of the request
         */
        Result setLogLevel(GenSS context, LogLevel level);
        
        /**
         * Request that the specified system be reconnected
         * @param context the context of the state machine
         * @return object describing the result of the request
         */
        Result reconnect(GenSS context);
    }

    /**
     * Default implementations of state method to be overridden in derived
     * classes.
     */
    abstract class ModeBase implements Mode {

        public void onExit(GenSS context) throws Exception {
        }
        
        public Result shutdown(GenSS context, ILifeCycleServer.Shutdown behaviour) {
            restart = (behaviour == ILifeCycleServer.Shutdown.RESTART);
            context.setMode(ModeEnum.SHUTDOWN);
            return new Result();
        }
        
        public Result setLogLevel(GenSS context, LogLevel level) {
            Logger.getRootLogger().setLevel(level.getLog4jLevel());
            logger.info(String.format("Set log level to: %s", level.toString()));
            return new Result();
        }
        
        public Result reconnect(GenSS context) {
            String message = String.format("%s can't reconnect", getClass());
            logger.error(message);
            return new Result(SuccessCode.COMPLETE_FAILURE, message);
        }

    }

    /**
     * Initialize mode state object
     */
    class Initialize extends ModeBase {

        public void onEnter(GenSS context) throws Exception {
            context.setState(State.INACTIVE);
            // now see if derived class has anything to do
            try {
                context.duringInitialize();
            } catch (Exception ex) {
                logger.error("Exception in Initialize state", ex);
                context.setMode(ModeEnum.SHUTDOWN);
            }
        }

    }

    /**
     * Manage mode state object.
     */
    class Manage extends ModeBase {

        public void onEnter(GenSS context) throws Exception {
            context.setState(State.OPERATIONAL);
        }
    }

    /**
     * Shutdown mode state object.
     */
    class Shutdown extends ModeBase {

        public void onEnter(GenSS context) throws Exception {
            context.setState(State.INACTIVE);
            // do the notify prior to starting the potentially time consuming
            // shutdown stuff. That will place a timeout on the shutdown.
            shutdownStartedSemaphore.release();
            // deregister the services
            for (IAcceptor acceptor : acceptorMap.keySet()) {
                ServiceURN serviceURN = acceptorMap.get(acceptor);
                URI serviceLocationURI = getServiceLocationURI(acceptor.getProtocol(), serviceURN);
                deregisterService(serviceURN, serviceLocationURI);
            }
            // stop the subjects
            sensorSubject.stop();
            alarmSubject.stop();
            lifeCycleServer.stop();
            // wait briefly for tx queues to clear
            // FIXME closing the actual connections should probably ensure the queues are clear
            Thread.sleep(TX_QUEUE_CLEAR_DELAY);
            // disable the acceptors
            for (IAcceptor acceptor : acceptorMap.keySet()) {
                logger.debug("Closing " + acceptor);
                try {
                    acceptor.close();
                } catch (Exception ex) {
                    logger.fatal("", ex);
                    // TODO recover or continue shutdown?????
                }
            }
            acceptorMap.clear();
            // teminate services?
            try {
                context.duringShutdown();
            } catch (Exception ex) {
                logger.error("Error during shutdown", ex);
            }
        }

        @Override
        public Result shutdown(GenSS context, ILifeCycleServer.Shutdown behaviour) {
            logger.info(String.format("%s received %s request, already shutting down", getClass(), behaviour));
            return new Result(SuccessCode.PARTIAL_SUCCESS, "Already in shutdown mode");
        }
    }

    /**
     * ShutdownComplete mode state object.
     */
    class ShutdownComplete extends ModeBase {

        public void onEnter(GenSS context) throws Exception {
            shutdownCompleteSemaphore.release();
        }

        @Override
        public Result shutdown(GenSS context, ILifeCycleServer.Shutdown behaviour) {
            logger.info(String.format("%s received %s request, shutdown already complete", getClass(), behaviour));
            return new Result(SuccessCode.PARTIAL_SUCCESS, "Shutdown already complete");
        }
    }

    /** The current mode of the Mode state machine */
    private Mode mode;

    protected Mode initialize = new Initialize();
    protected Mode manage = new Manage();
    protected Mode shutdown = new Shutdown();
    protected Mode shutdownComplete = new ShutdownComplete();

    /* ***********************************************************************
     * Start of public API
     *************************************************************************/

    /**
     * Constructor
     * 
     * @param systemURN the system identifier
     */
    public GenSS(SystemURN systemURN) throws URISyntaxException {
        this(systemURN, new SensorSubject(), new AlarmSubject(), new LifeCycleServer());
    }
    
    /**
     * Constructor for more detailed control
     * 
     * @param systemURN the system identifier
     * @param sensorSubject the sensor subject object
     * @param alarmSubject the alarm subject object
     * @param lifeCycleServer the lifecycle server object
     */
    public GenSS(SystemURN systemURN, SensorSubject sensorSubject, AlarmSubject alarmSubject, LifeCycleServer lifeCycleServer) throws URISyntaxException {
        if (systemURN == null) {
            String message = "Can't specify null systemURN";
            logger.error(message);
            throw new IllegalArgumentException(message);
        }
        Runtime.getRuntime().addShutdownHook(new Thread("Thread-" + getClass().getCanonicalName()) {
        	@Override
        	public void run() {
        		if (mode != null) {
		    		mode.shutdown(GenSS.this, ILifeCycleServer.Shutdown.HALT);
		    		try {
		    			// wait for rest of shutdown process to complete
						shutdownInterruptedSemaphore.tryAcquire(SHUTDOWN_TIMEOUT, TimeUnit.MILLISECONDS);
					} catch (InterruptedException e) {
						logger.error("Interrupted exception in timeout hook.", e);
					}
        		}
        	}
        });
        this.systemURN = systemURN;
        sensorsURN = new ServiceURN(systemURN, "sensors");
        alarmsURN = new ServiceURN(systemURN, "alarms");
        lifecycleURN = new ServiceURN(systemURN, "lifecycle");
        sensorSubject.setTerminateListener(terminateListener);
        this.sensorSubject = sensorSubject;
        this.alarmSubject = alarmSubject;
        this.stateSensor.setStatusStrategy(StatusStrategyFactory.createStatusStrategy(this.stateSensor, "Discrete:INACTIVE=>UNKNOWN,OPERATIONAL=>NOMINAL"));
        this.modeSensor.setStatusStrategy(StatusStrategyFactory.createStatusStrategy(this.modeSensor, "Discrete:INITIALIZE=>UNKNOWN,MANAGE=>NOMINAL,SHUTDOWN=>FAILURE,SHUTDOWN_COMPLETE=>FAILURE"));
        lifeCycleServer.addSystem(systemURN, new ILifeCycleServer.ICallback() {

            public Result setLogLevel(LogLevel level) {
                return mode.setLogLevel(GenSS.this, level);
            }

            public Result shutdown(ILifeCycleServer.Shutdown behaviour) {
                return mode.shutdown(GenSS.this, behaviour);
            }
            
            public Result reconnect() {
                return mode.reconnect(GenSS.this);
            }
            
            public Result ping() {
            	return new Result();
            }
            
        });
        this.lifeCycleServer = lifeCycleServer;
    }

    /**
     * Initialize the state machines of the GenSS. Starts their thread but only
     * enters initialize mode if there is a valid subsystemURI.
     */
    public void initialize() {
    	// ensure that all shutdown semaphores are drained
        shutdownStartedSemaphore.drainPermits();
        shutdownCompleteSemaphore.drainPermits();
        shutdownInterruptedSemaphore.drainPermits();
        // start the threads for the state machines
        modeQueue.clear();
        modeThread = new ModeRunner(this);
        modeThread.start();

        // and then set the first mode which will itself set the state
        if (systemURN != null) {
            setMode(ModeEnum.INITIALIZE);
        }
    }

    /**
     * Waits for notification that shutdown has started. Then waits for a
     * timeout period before returning with the restart flag
     * @return true if the genss should be restarted
     */
    public boolean waitHalt() {
        try {
            shutdownStartedSemaphore.acquire();
            logger.warn(String.format("%s request received, going down in %dms", restart ? "Restart" : "Halt", SHUTDOWN_TIMEOUT));
            shutdownCompleteSemaphore.tryAcquire(SHUTDOWN_TIMEOUT, TimeUnit.MILLISECONDS);
        } catch (InterruptedException ex) {
            logger.error("Interrupted while waiting for GenSS to halt");
        }
        logger.warn("Going down now!");
        shutdownInterruptedSemaphore.release();
        return restart;
    }

    /**
     * Shutdown the state machine of the GenSS.
     */
    public void shutdown() {
        // if the not already shutting down then trigger a shutdown and wait
        // for completion
        if (mode == initialize || mode == manage) {
            mode.shutdown(this, ILifeCycleServer.Shutdown.HALT);
            waitHalt();
        }
        // now stop the mode thread if necessary
        if (modeThread != null) {
            modeThread.terminate(BaseRunner.JOIN_TIMEOUT);
            modeThread = null;
        }
        try {
			TcpSelector.getInstance().close();
		} catch (IOException e) {
			logger.error("Exception during TcpSelector.close", e);
		}
    }
    
    public IAlarmSubject getAlarmSubject() {
        return alarmSubject;
    }

    public ILifeCycleServer getLifeCycleServer() {
        return lifeCycleServer;
    }

    public ISensorSubject getSensorSubject() {
        return sensorSubject;
    }

    public SystemURN getSystemURN() {
        return systemURN;
    }
    
    public ServiceURN getLifeCycleServiceURN() {
    	return lifecycleURN;
    }
    
    public ServiceURN getSensorsServiceURN() {
    	return sensorsURN;
    }
    
    public ServiceURN getAlarmsServiceURN() {
    	return alarmsURN;
    }
    
    public URI getServiceLocationURI(String protocol, ServiceURN serviceURN) throws URISyntaxException {
        IAcceptor acceptor = getServiceAcceptor(protocol, serviceURN);
        return getServiceLocationURI(acceptor, serviceURN);
    }
    
    /**
     * Check if the genSS is operational
     * @return true if genss is operational
     */
    public boolean isOperational() {
        if (stateSensor == null) {
            return false;
        }
        if (stateSensor.getValue() == null) {
            return false;
        }
        if (stateSensor.getValue().getValue() == null) {
            return false;
        }
        return stateSensor.getValue().getValue().equals(State.OPERATIONAL.toString());
    }
    
    /* ***********************************************************************
     * Protected methods which can be overridden
     *************************************************************************/

    /**
     * Add a sensor to the sensor subject
     * 
     * @param systemURN the URN of the system that contains the sensor
     * @param sensor the sensor object to add
     * @return the URI allocated to the sensor
     */
    protected SensorURN addSensor(SystemURN systemURN, ISensor sensor) throws URISyntaxException {
        SensorURN sensorURN = new SensorURN(systemURN, sensor.getName());
        sensorSubject.addSensor(sensorURN, sensor);
        for (IAcceptor acceptor: acceptorMap.keySet()) {
            if (acceptorMap.get(acceptor).equals(sensorsURN)) {
                registerService(sensorURN, getServiceLocationURI(acceptor, sensorURN));
            }
        }
        return sensorURN;
    }
    
    /**
     * Try to remove a sensor from the sensor subject. If successful then 
     * deregister the sensor
     * 
     * @param sensorURN the URN of the sensor to remove
     * @return true if the sensor successfully removed
     */
    protected boolean removeSensor(SensorURN sensorURN) throws URISyntaxException {
        if (sensorSubject.removeSensor(sensorURN)) {
            for (IAcceptor acceptor: acceptorMap.keySet()) {
                if (acceptorMap.get(acceptor).equals(sensorsURN)) {
                    deregisterService(sensorURN, getServiceLocationURI(acceptor, sensorURN));
                }
            }
            return true;
        }
        return false;
    }

    /**
     * Add an acceptor object for an interface of the GenSS for the specified
     * service.
     */
    protected void addAcceptor(IAcceptor acceptor, ServiceURN serviceURN) {
        acceptorMap.put(acceptor, serviceURN);
        try {
            URI serviceLocationURI = getServiceLocationURI(acceptor, serviceURN);
            registerService(serviceURN, serviceLocationURI);
        } catch (URISyntaxException ex) {
            logger.error("Failure to register service", ex);
        }
    }
    
    /**
     * Sets the state by adding it to the state queue.
     * @param state the desired state
     */
    protected void setState(State state) throws InvalidSensorValue {
        ISensorValue value = new SensorValue(state.toString());
        logger.info(String.format("Changing state to %s", state.toString()));
        stateSensor.setValue(value);
    }

    /**
     * Sets the mode by adding it to the mode queue.
     * @param mode the desired mode
     */
    protected void setMode(ModeEnum newMode) {
        modeQueue.add(newMode);
    }

    /**
     * Override this method to register a service in derived classes.
     * @param serviceURN the service URI
     * @param serviceLocationURI the service location URI
     */
    protected abstract void registerService(ServiceURN serviceURN, URI serviceLocationURI);

    /**
     * Override this method to deregister a service in derived classes.
     * @param serviceURN the service URI
     * @param serviceLocationURI the service location URI
     */
    protected abstract void deregisterService(ServiceURN serviceURN, URI serviceLocationURI);

    /**
     * Override this method in derived classes to expand initialize mode
     * behaviour.
     */
    protected void duringInitialize() throws Exception {
    }
    
    /**
     * Override this method to expand shutdown behaviour in derived classes.
     * 
     * @param context the GenSS
     */
    protected void duringShutdown() throws Exception {
    	if (timer != null) {
    		timer.cancel();
    		timer = null;
    	}
    }
    
    /**
     * Configure the acceptors for a particular service.
     * @param urls comma separated list of URIs
     * @param acceptListener accept listener for new connections
     * @param serviceURN the service URN
     * @throws Exception
     */
    protected void internalSetupAcceptors(String urls, IAcceptListener acceptListener, ServiceURN serviceURN) throws Exception {
        for (String url : urls.split(",")) {
            URI acceptorURI = new URI(url);
            addAcceptor(AcceptorFactory.createAcceptor(acceptorURI, acceptListener), serviceURN);
        }
    }
    
    /**
     * Call this to configure acceptors for the GenSS external interfaces
     * @param lifecycleURLs list of comma separated URIs
     * @param sensorURLs list of comma separated URIs
     * @param alarmURLs list of comma separated URIs
     * @throws Exception
     */
    protected void setupAcceptors(String lifecycleURLs, String sensorURLs, String alarmURLs) throws Exception {
        internalSetupAcceptors(lifecycleURLs, new LifeCycleAcceptListener(getLifeCycleServer()), lifecycleURN);
        internalSetupAcceptors(sensorURLs, new SensorAcceptListener(getSensorSubject()), sensorsURN);
        internalSetupAcceptors(alarmURLs, new AlarmAcceptListener(getAlarmSubject()), alarmsURN);
        addSensors();
    }
    
    /**
     * Add sensors for the subsystem in this and derived methods. This is called
     * after the acceptors have been setup.
     * @throws URISyntaxException
     */
    protected void addSensors() throws URISyntaxException {
        // add state and mode sensors
        addSensor(systemURN, stateSensor);
        addSensor(systemURN, modeSensor);
    }
    
    /**
     * Get a set of acceptors for the specified service.
     * @param serviceURN the specified service
     * @return a set of acceptors for this service
     */
    protected Set<IAcceptor> getServiceAcceptors(ServiceURN serviceURN) {
        Set<IAcceptor> acceptors = new HashSet<IAcceptor>();
        for (IAcceptor acceptor : acceptorMap.keySet()) {
            if (acceptorMap.get(acceptor).equals(serviceURN)) {
                acceptors.add(acceptor);
            }
        }
        return acceptors;
    }
    
    /** Access the timer object 
     * @return the timer object
     */
    synchronized protected Timer getTimer() {
    	if (timer == null) {
    		timer = new Timer("Timer-" + getClass().getCanonicalName());
    	}
    	return timer;
    }

    /* ***********************************************************************
     * Private methods for internal use
     *************************************************************************/
    
    /**
     * Search through map of all acceptors for one for the specified serviceURN
     * with a matching protocol.
     * @param protocol desired acceptor protocol
     * @param serviceURN the service URN to look for
     * @return valid acceptor or null if not found
     */
    private IAcceptor getServiceAcceptor(String protocol, ServiceURN serviceURN) {
        for (IAcceptor acceptor : getServiceAcceptors(serviceURN)) {
            if (acceptor.getProtocol().equals(protocol)) {
                return acceptor;
            }
        }
        // couldn't find a match
        return null;
    }
    
    private URI getServiceLocationURI(IAcceptor acceptor, ServiceURN serviceURN) throws URISyntaxException {
        if (acceptor != null) {
            return new URI(String.format("%s/%s", acceptor.getBaseLocationURI().toString(), serviceURN.getService()));
        }
        return null;
    }

}
